package ms.unicorn.dsl.runtime.interpreter;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import ms.unicorn.Entity;
import ms.unicorn.dsl.impl.LanguageElementField;
import ms.unicorn.dsl.runtime.ExecutionContext;
import ms.unicorn.dsl.runtime.IndexedMemberProvider;
import ms.unicorn.dsl.runtime.InvocationProvider;
import ms.unicorn.dsl.runtime.SphynxInterpreter;
import ms.unicorn.dsl.runtime.VariableStorage;
import ms.unicorn.dsl.runtime.impl.VariableImpl;
import ms.unicorn.dsl.unicornDSL.CallParameter;
import ms.unicorn.dsl.unicornDSL.FunctionArgument;
import ms.unicorn.dsl.unicornDSL.IdentifiedElement;
import ms.unicorn.dsl.unicornDSL.UnicornDSLPackage;
import ms.unicorn.dsl.unicornDSL.XExpression;
import ms.unicorn.dsl.unicornDSL.XFeatureCall;
import ms.unicorn.dsl.unicornDSL.XFunctionDeclaration;
import ms.unicorn.dsl.unicornDSL.impl.IdentifiedElementImpl;
import ms.unicorn.dsl.unicornDSL.impl.XFeatureCallImpl;
import ms.unicorn.dsl.unicornDSL.impl.XMemberTermImpl;
import ms.unicorn.dsl.utils.ExceptionUtil;
import ms.unicorn.dsl.utils.Invocator;
import ms.unicorn.dsl.utils.ReflectionUtil;
import ms.unicorn.dsl.utils.SphynxInterpreterMissingException;
import ms.unicorn.dsl.utils.SphynxInvalidIndexException;
import ms.unicorn.dsl.utils.SphynxMemberNotFoundException;
import ms.unicorn.dsl.utils.SphynxNullReferenceExpcetion;
import ms.unicorn.dsl.utils.SphynxReadonlyException;
import ms.unicorn.dsl.utils.SphynxRuntimeException;
import ms.unicorn.dsl.utils.SphynxUninitializedAccessException;
import ms.unicorn.util.NamedMemberProvider;

import org.eclipse.xtext.nodemodel.INode;
import org.eclipse.xtext.nodemodel.util.NodeModelUtils;


public class XFeatureCallInterpreter implements RuntimeErrorMessage {

	public static class MemberStorage extends VariableImpl {
		private Object host;
		private XFeatureCall featureCall;
		private SphynxInterpreter interpreter;
		private ExecutionContext context;
		private String featureName;
		
		public MemberStorage(SphynxInterpreter interpreter, Object host, String featureName, XFeatureCall featureCall, ExecutionContext context) {
			super(VARIABLE, false);
			this.interpreter = interpreter;
			this.host = host;
			this.featureCall = featureCall;
			this.context = context;
			this.featureName = featureName;
			super.setValue(VariableStorage.UNEVALUATED);
		}
		
		public static MemberStorage createMemberStorage(SphynxInterpreter interpreter, Object host, String featureName, XFeatureCall featureCall, ExecutionContext context) {
			MemberStorage m = new MemberStorage(interpreter, host, featureName, featureCall, context);
			try {
				Object v = m.getValue();
			}
			catch(Exception x) {
				return null;
			}
			return m;
		}
		
		@Override
		public Object getValue() {
			if (super.getValue() == VariableStorage.UNEVALUATED) {
				if (featureCall.isIndexCall()) { // index access
					Object feature = featureName != null ? getNamedMemberValue(host, featureName) : host;
					if (feature != null)
						super.setValue(getIndexedMemberValue(feature));
					else if (!featureCall.isNullSafe())
						throw new SphynxNullReferenceExpcetion(context, featureCall);
					else
						super.setValue(null);
				} else { // member variable asset
					super.setValue(getNamedMemberValue(host, featureName));
				}
			}
			return super.getValue();
		}
		
		@Override
		public Object setValue(Object value) {
			if (featureCall.isIndexCall()) {
				Object feature = featureName != null ? getNamedMemberValue(host, featureName) : host;
				if (feature != null)
					setIndexedMemberValue(feature, value);
				else if (!featureCall.isNullSafe())
					throw new SphynxNullReferenceExpcetion(context, featureCall);
			} else {
				setNamedMemberValue(host, featureName, value);
			}
			return super.setValue(value);
		}

		
		public Object getIndexedMemberValue(Object feature) {
			if (feature instanceof IndexedMemberProvider) {
				IndexedMemberProvider indexedFeature = (IndexedMemberProvider) feature;
				if (!indexedFeature.isInitialized())
					throw new SphynxUninitializedAccessException(context, featureCall);
					
				List<Object> args = new ArrayList<Object>(featureCall.getParams().size());
				for(CallParameter param: featureCall.getParams())
					if (param.isInheritedParameters())
						throw new SphynxRuntimeException(context, featureCall, DOTDOTDOT_IS_NOT_SUPPORTED);
					else
						args.add(interpreter.resolveValue(param.getValue(), context));
				if (!indexedFeature.hasMember(args.toArray()))
					throw new SphynxInvalidIndexException(context, featureCall);
				return indexedFeature.getMember(args.toArray());
			}
			
			int indexLen = featureCall.getParams().size();
			if (indexLen != 1)
				throw new SphynxInvalidIndexException(context, featureCall);
			
			Object index = interpreter.resolveValue(featureCall.getParams().get(0).getValue(), context);
			return getIndexValue(feature, index);
		}

		public void setIndexedMemberValue(Object feature, Object value) {
			if (feature instanceof IndexedMemberProvider) {
				IndexedMemberProvider indexedFeature = (IndexedMemberProvider) feature;
				
				if (!indexedFeature.isInitialized())
					throw new SphynxUninitializedAccessException(context, featureCall);
				if (!indexedFeature.isWritable())
					throw new SphynxReadonlyException(context, featureCall);
				
				List<Object> args = new ArrayList<Object>(featureCall.getParams().size());
				for(CallParameter param: featureCall.getParams())
					if (param.isInheritedParameters())
						throw new SphynxRuntimeException(context, featureCall, DOTDOTDOT_IS_NOT_SUPPORTED);
					else
						args.add(interpreter.resolveValue(param.getValue(), context));
				((IndexedMemberProvider) feature).setMember(value, args);
			}
			else {
				int indexLen = featureCall.getParams().size();
				if (indexLen != 1)
					throw new SphynxInvalidIndexException(context, featureCall);

				Object index = interpreter.resolveValue(featureCall.getParams().get(0).getValue(), context);
				setIndexValue(feature, index, value);
			}
		}

		private static Object getIndexValue(Object feature, Object index) {
			if (feature instanceof List<?>)
				return ((List<?>) feature).get((Integer)index);
			if (feature instanceof Map<?, ?>)
				return ((Map<?, ?>) feature).get(index);
			
			return Array.get(feature, (Integer) index);
		}

		@SuppressWarnings({ "unchecked", "rawtypes" })
		private static void setIndexValue(Object feature, Object index, Object value) {
			if (feature instanceof List<?>)
				((List) feature).set((Integer)index, value);
			else if (feature instanceof Map<?, ?>)
				((Map) feature).put(index, value);
			else
			Array.set(feature, (Integer) index, value);
		}

		public Object getNamedMemberValue(Object owner, String featureName) {
			Object obj = null;
			obj = null;
			if (owner instanceof NamedMemberProvider) {
				NamedMemberProvider namedFeature = (NamedMemberProvider) owner;
				
				if (!namedFeature.isInitialized())
					throw new SphynxUninitializedAccessException(context, featureCall);
				if (namedFeature.hasMember(featureName))
					//throw new SphynxMemberNotFoundException(context, featureName);
					return namedFeature.getMember(featureName);
				//obj = namedFeature.getMember(featureName);
				/*if (obj instanceof VariableStorage)
					obj = ((VariableStorage) obj).getValue();*/
				//return obj;
			}
			
			obj = ReflectionUtil.getPropertyValue(owner, featureName, VariableStorage.EMPTY);
			if (obj == VariableStorage.EMPTY) 
				throw new SphynxRuntimeException(context, featureCall, String.format(MEMBER_IS_NOT_ACCESSIABLE, featureName, owner.getClass().getName()));
			return obj;
		}

		public void setNamedMemberValue(Object owner, String featureName, Object value) {
			if (owner instanceof NamedMemberProvider) { 
				NamedMemberProvider namedFeature = (NamedMemberProvider) owner;
				
				if (!namedFeature.isInitialized())
					throw new SphynxUninitializedAccessException(context, featureCall);
				if (!namedFeature.isWritable())
					throw new SphynxReadonlyException(context, featureCall);
				if (!namedFeature.hasMember(featureName))
					throw new SphynxMemberNotFoundException(context, featureName);
				
				namedFeature.setMember(featureName, value);
			}
			else 
				ReflectionUtil.setPropertyValue(owner, featureName, value);
		};
	
		
	}

	public static Object _evaluate(SphynxInterpreter interpreter, XFeatureCallImpl featureCall, ExecutionContext context) {				
		//IdentifiedElement id = featureCall.getFeatureElement();
		String featureName = getFeatureName(featureCall);
		
		if (featureCall.isOperatorCall()) { // call to a script function
			
			// passing variable
			XFunctionDeclaration funcDef = null;			
			Object obj = getVariableValue(interpreter, featureCall, context);
			
			if (obj instanceof XFunctionDeclaration)
				funcDef = (XFunctionDeclaration) obj;
			
			if (funcDef != null) {     
				//if (funcDef instanceof XPatternDeclaration) 
				//	return new PatternMatcher().matchPattern(interpreter, (XPatternDeclaration) funcDef, featureCall, context, null);
				//else {
					ExecutionContext calleeContext = context.createCalleeContext();
					passParameters(interpreter, funcDef, featureCall, context, calleeContext);
					return XFunctionDeclartationInterpreter.interpret(interpreter, funcDef, calleeContext);
					//return interpreter.evaluate(funcDef, calleeContext);
				//}
			}
			else if (!featureCall.isNullSafe())
				throw new SphynxRuntimeException(context, featureCall, String.format(VALID_FUNCTION_REFERENCE_EXPECTED, featureName));
		}
		else if (featureCall.isIndexCall()) { // access to an array or index support variable
			Object obj = getVariableValue(interpreter, featureCall, context);
			// create a new MemberStorage to handle this case
			return new MemberStorage(interpreter, obj, null, featureCall, context);
			/*// do something			
			if (obj != null)
				return getIndexedMemberValue(interpreter, obj, featureCall, context);
			else if (!featureCall.isNullSafe())
				throw new SphynxNullReferenceExpcetion(context, featureCall);*/
		}
		else // access to a variable
			return getVariable(interpreter, featureCall, context);		
		return null;
	}
	
	/*public static Object _evaluate(SphynxInterpreter interpreter, XPatternClassDeclarationImpl exp, ExecutionContext context) {
		ExecutionContext calleeContext = context.createCalleeContext();
		for(IdentifiedElement e:  exp.getArguments()) {
			FunctionArgument arg = (FunctionArgument) e;
			calleeContext.allocateVariable(VariableStorage.ARGUMENT, false, arg.getName(), interpreter.resolveValue(arg.getValue(), context));
		}
		return calleeContext;
		return exp;
	}*/
	
	public static Object _evaluate(SphynxInterpreter interpreter, XMemberTermImpl term, ExecutionContext context) {
		Object owner = interpreter.resolveValue(term.getOwner(), context);
		if (owner == null)
			owner = context.getThisObject();
		// TODO: resolve this cheat
		// Special treatment for case: owner is a LanguageElementField and context.thisObject() is an ElementEntity
		if (owner instanceof LanguageElementField && context.getThisObject() instanceof Entity) {
			Entity thisObject = (Entity) context.getThisObject();
			owner = thisObject.eGet(((LanguageElementField) owner).getAttribute());
		}
		
		XFeatureCall featureCall = (XFeatureCall) term.getMember();
		String featureName = getFeatureName(featureCall);
		
		if (owner != null) {
			if (featureCall.isOperatorCall()) { // call a member method
				if (owner instanceof InvocationProvider) // is owner support MemberCallProvider interface?
					return ((InvocationProvider) owner).invoke(interpreter, featureName, featureCall, context);
				if (owner instanceof NamedMemberProvider) {
					Object obj = ((NamedMemberProvider) owner).getMember(featureName);
					if (obj instanceof VariableStorage)
						obj = ((VariableStorage) obj).getValue();
					if (obj instanceof XFunctionDeclaration) {
						XFunctionDeclaration func = (XFunctionDeclaration)obj;
						ExecutionContext callContext = context.createCalleeContext();
						passParameters(interpreter, func, featureCall, context, callContext);
						return interpreter.evaluate(func, callContext);
					}
					if (obj != null && !term.isNullSafe())
						throw new SphynxRuntimeException(context, term, String.format("Member '%s' of type '%s' is not invocable.", featureName, owner.getClass().getName()));
					//return null;
				}
				List<Object> args = new ArrayList<Object>(featureCall.getParams().size());
				for(CallParameter param: featureCall.getParams())
					if (param.isInheritedParameters())
						throw new SphynxRuntimeException(context, term, "'...' is not supported for java reflection call");
					else
						args.add(interpreter.resolveValue(param.getValue(), context));
				try {
					return Invocator.invoke(owner, featureName, args.toArray());
				}
				catch (RuntimeException e) {
					throw new SphynxRuntimeException(context, featureCall, e.getMessage(), e);
				}
			} 
			else {
				return new MemberStorage(interpreter, owner, featureName, featureCall, context);
			}
			/*else if (featureCall.isIndexCall()) { // index access
				Object feature = getNamedMemberValue(owner, featureName, term, context);
				if (feature != null)
					return getIndexedMemberValue(interpreter, feature, featureCall, context);
				else if (!term.isNullSafe())
					throw new SphynxNullReferenceExpcetion(context, term);
			} else { // member variable asset
				return getNamedMemberValue(owner, featureName, term, context);
			}*/
		}
		else if (!term.isNullSafe())
			throw new SphynxNullReferenceExpcetion(context, term);
		return null;
	}
		
	public static String getFeatureName(XFeatureCall feature) {
		IdentifiedElement id = feature.getFeatureElement();
		String name = id != null ? id.getName() : feature.getFeature();
		
		if (name == null) {
			List<INode> node = NodeModelUtils.findNodesForFeature(feature, UnicornDSLPackage.eINSTANCE.getXFeatureCall_FeatureElement());
			if (node.size() > 0)
				name = node.get(0).getText().trim();
		}
		
		return name;
	}
		
	private static Object getVariable(SphynxInterpreter interpreter, XFeatureCall feature, ExecutionContext context) {
		IdentifiedElement id = feature.getFeatureElement();
		String name = getFeatureName(feature);
		
		if ("__context".equals(name))
			return context;
		if ("__interpreter".equals(name))
			return interpreter;
		if ("this".equals(name))
			if (context.getThisObject() != null)
				return context.getThisObject();
			else
				throw new SphynxRuntimeException(context, feature, "The 'this' object is not available in this context");
		
		if (id instanceof XFunctionDeclaration)
			return id;
		// check for local variable
		VariableStorage variable = context.getVariable(name, false, true);
		if (variable == null && context.getThisObject() != null)
			variable = MemberStorage.createMemberStorage(interpreter, context.getThisObject(), name, feature, context);
		if (variable != null)
			return variable;
		// or is it a global variable?
		if (id instanceof IdentifiedElement && !(id.getClass() ==  IdentifiedElementImpl.class))
			return interpreter.evaluate(id, context.getRootContext());
		throw new SphynxRuntimeException(context, feature, "Unknown identifier '" + name + "'");
	}
		
	private static Object getVariableValue(SphynxInterpreter interpreter, XFeatureCall feature, ExecutionContext context) {
		Object obj = getVariable(interpreter, feature, context);
		while (obj instanceof VariableStorage)
			obj = ((VariableStorage) obj).getValue();
		return obj;
	}
	
	static void passParameters(SphynxInterpreter interpreter,
			XFunctionDeclaration func, XFeatureCall call, ExecutionContext context,
			ExecutionContext callContext) {
		
		// DONE 
		// revise the parameter passing, to save the CPU, do not evaluate the default value of parameter
		// until we are sure that it is not passed.
		
		// 1st round, allocation for arguments
		List<VariableStorage> args = new ArrayList<VariableStorage>();
		boolean allowAnyPass = false;
		for(XExpression e : func.getArguments()) {
			FunctionArgument arg = (FunctionArgument) e;
			if (arg.isAny())
				allowAnyPass = true;
			else 
				args.add(callContext.allocateVariable(VariableStorage.ARGUMENT, false, arg.getName(), VariableStorage.EMPTY));
		}
		// 2nd round, matching arguments
		int idx = 0; 
		boolean namePassed = false;
		boolean anyPassed = false;
		List<Object> anyArgs = allowAnyPass? new ArrayList<Object>() : null;
		
		for(CallParameter param: call.getParams()) {
			Object value = param.getValue() != null ? interpreter.evaluate(param.getValue(), context) : null;
			if (value instanceof VariableStorage)
				value = ((VariableStorage) value).getValue();
			
			if (param.isInheritedParameters())
				if (anyPassed) 
					throw new SphynxRuntimeException(context, call, String.format(PARAMETER_ALDREADY_PASSED, param.getName()));
				else anyPassed = true;
			else if (param.getName() == null) // passed by location
				if (namePassed)
					throw new SphynxRuntimeException(context, call, NAMED_PASSING_REQUIRED);
				else if (idx < args.size())
					args.get(idx).setValue(value);
				else if (allowAnyPass) {
					anyArgs.add(value);
				}
				else
					throw new SphynxRuntimeException(context, call, TOO_MANY_PARAMETER_PASSING);
			else { // passed by name
				namePassed = true;
				VariableStorage var = callContext.getVariable(param.getName(), false, false);
				if (var != null)
					if (var.isEmpty())
						var.setValue(value);
					else
						throw new SphynxRuntimeException(context, call, String.format(PARAMETER_ALDREADY_PASSED, param.getName()));
				else if (allowAnyPass) { 
					anyArgs.add(value);
					callContext.allocateVariable(VariableStorage.UNUSED_ARGUMENT, false, param.getName(), value);
				}
				else
					throw new SphynxRuntimeException(context, call, String.format(UNDECLARED_ARGUMENT_PASSED, param.getName()));
			}
			idx++;	
		}
		// 3rd round, passing 'any' parameters
		if (anyPassed) {
			for (String argName: context.getVariableNames(VariableStorage.UNUSED_ARGUMENT)) {
				VariableStorage var = callContext.getVariable(argName, false, false);
				VariableStorage arg = context.getVariable(argName, false, false);
				if (var != null) {
					if (var.isEmpty())
						var.setValue(arg.getValue());
				}
				else if (allowAnyPass) 
					callContext.allocateVariable(VariableStorage.UNUSED_ARGUMENT, false, argName, arg.getValue());
			}
		}
		//4th round, check for uninitialized parameters
		// hack: passing this object to callContext
		if (context.getThisObject() != null)
			callContext.allocateVariable(VariableStorage.READONLY, false, "__callerThis", context.getThisObject());
		for (int i = 0; i < args.size(); i++)
			if (args.get(i).isEmpty()) {
				FunctionArgument arg = (FunctionArgument) func.getArguments().get(i);
				if (arg.getValue() != null) {
					Object value = null;
					try {
						value = interpreter.resolveValue(arg.getValue(), context);
					}
					catch (SphynxInterpreterMissingException e) { // this 
						if (e.getElement() instanceof FunctionArgument)
							value = interpreter.resolveValue(arg.getValue(), callContext);
						else throw e;
					}
					//if (value instanceof VariableStorage)
					//	value = ((VariableStorage) value).getValue();
					args.get(i).setValue(value);
				}
				else 
					throw new SphynxRuntimeException(context, call, 
							String.format(UNINITIALIZED_ARGUMENT, arg.getName()));
			}
		
		if (anyArgs != null)
			callContext.allocateVariable(VariableStorage.READONLY, false, "__args", anyArgs.toArray());
	}
}
